package com.nts.visionworks.core.multipart.controller;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.activation.MimetypesFileTypeMap;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.util.FileCopyUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.commons.CommonsMultipartFile;
import org.springframework.web.servlet.ModelAndView;

import com.nts.visionworks.core.multipart.MultipleFileSupportMultipartResolver;
import com.nts.visionworks.core.multipart.OutputFileStreamView;
import com.nts.visionworks.core.multipart.domain.Archive;
import com.nts.visionworks.core.multipart.domain.Multipart;
import com.nts.visionworks.core.multipart.service.MultipartService;
import com.nts.visionworks.core.utility.GuidUtil;

/**
 * 멀티파트 요청에 대응하는 Request Mapping을 제공한다.
 * @author Devcken
 */
@Controller("multipartController")
@RequestMapping("/archive")
public class MultipartController
{
	private static final Logger logger = LoggerFactory.getLogger(MultipartController.class);
	
	/**
	 * 아카이브 영속성을 위한 서비스 객체를 나타낸다.
	 */
	@Resource(name="multipartService")
	private MultipartService _multipartService;
	
	/**
	 * 멀티 요청에 의해 파일(분할 파일 포함)을 업로드한다.
	 * @param multipart 멀티파트 리졸버에 의해 전달된 멀티파트 영속성 인스턴스로 멀티파트 정보를 담고 있다.
	 * @param bindingResult HTTP 요청에 대한 바인딩 결과를 나타낸다. 
	 * @param request HTTP 요청으로 멀티파트 요청으로 변환되기 전 상태를 나타낸다.
	 * @param response HTTP 응답으로 멀티파트 요청에 대한 응답을 나타낸다.
	 * @param session 멀티파트 요청에 상응하는 HTTP 세션을 나타낸다.
	 * @return 업로드된 아카이브 정보를 JSON 형식으로 응답하는 뷰로 전달한다.
	 */
	@RequestMapping(value="/upload")
	public ModelAndView upload(Multipart multipart, BindingResult bindingResult, HttpServletRequest request, HttpServletResponse response, HttpSession session)
	{
		Map<String, Object> results = new HashMap<String, Object>();
		
		try
		{
			Archive[] archives = this.uploadFiles(multipart, request, response, session);
			
			results.put("result", true);
			results.put("multipart", multipart);
			
			if (multipart.getBlobIndex() == multipart.getBlobCount())
			{
				results.put("archives", archives);
			}
		}
		catch (Exception error)
		{
			// 예외가 발생하면 실패 처리한다.
			results.put("result", false);
			
			logger.error(error.getMessage(), error);
		}
		
		return new ModelAndView("jsonView", results);
	}
	
	/**
	 * 멀티파트 정보로 전달된 멀티파트 파일들을 업로드한다.
	 * @param multipart 멀티파트 리졸버에 의해 전달된 멀티파트 영속성 인스턴스로 멀티파트 정보를 담고 있다.
	 * @param request HTTP 요청으로 멀티파트 요청으로 변환되기 전 상태를 나타낸다.
	 * @param response HTTP 응답으로 멀티파트 요청에 대한 응답을 나타낸다.
	 * @param session 멀티파트 요청에 상응하는 HTTP 세션을 나타낸다.
	 * @return 업로드된 파일들의 {@link com.nts.visionworks.core.multipart.domain.Archive} 정보 목록이 반환된다.
	 */
	private Archive[] uploadFiles(Multipart multipart, HttpServletRequest request, HttpServletResponse response, HttpSession session)
	{
		try
		{
			// 멀티파트 요청으로부터 넘어온 파일 목록을 가져온다.
			CommonsMultipartFile[] files = multipart.getArchives();
			
			// 요청된 파일이 없을 경우, 실패 처리한다.
			if (files == null || files.length == 0)
			{
				return new Archive[0];
			}
			
			Archive[] archives = new Archive[files.length];
			
			// 파일 목록을 조회하여 업로드 처리한다.
			for (int index = 0; index < files.length; index++)
			{
				// 파일 일련 번호가 존재하지 않을 경우, 파일 일부분을 업로드한다.(분할 전송)
				if (!multipart.getFileId().equals(""))
				{
					archives[index] = this.uploadFile(files[index], multipart.getFileId()
							, multipart.getFileName(), multipart.getBlobIndex(), multipart.getBlobCount(), request);
				}
				// 파일 일련 번호가 존재하면 해당 파일을 업로드 한다.
				else
				{
					archives[index] = this.uploadFile(files[index], request);
				}
			}
			
			return archives;
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
		}
		
		return new Archive[0];
	}
	
	/**
	 * 분할 파일을 업로드하고 모든 분할 파일이 업로드된 경우 최종 파일을 만든다. 
	 * @param file 한 개의 분할 파일을 나타내며, 모든 분할 파일들이 업로드될 때까지 임시적으로 저장된다. 
	 * @param fileId 최종 파일의 파일 일련 번호로 분할 파일 간 인식을 위해 사용된다.
	 * @param fileName 최종 파일의 실제 이름(확장자 포함)을 나타낸다.
	 * @param blobIndex 분할 파일의 인덱스로 분할 파일의 고유 번호(업로드 순서이기도 하다.)로 인식한다.
	 * @param blobCount 최종 파일에 대한 분할 파일의 전체 개수를 나타낸다.
	 * @param request 멀티파트 요청으로 멀티파트 분석을 하지 않은 요청 인스턴스이다.
	 * @return 업로드를 완료한 분할 파일 혹은 최종 파일의 아카이브 정보 인스턴스이다.
	 * @throws FileNotFoundException 분할 파일 혹은 최종 파일을 생성하지 못했을 때 발생할 수 있다.
	 * @throws IOException 분할 파일 혹은 최종 파일에 바이너리 데이터를 쓸 때 발생할 수 있다.
	 */
	private Archive uploadFile(CommonsMultipartFile file, String fileId, String fileName,
			int blobIndex, int blobCount, HttpServletRequest request) throws FileNotFoundException, IOException
	{
		Archive archive = new Archive();
		
		FileOutputStream blobOutput = null;
		
		try
		{
			// 업로드하려는 파일의 확장자를 추려낸다.
			String extension = fileName.lastIndexOf('.') > -1 ? fileName.substring(fileName.lastIndexOf('.') + 1, fileName.length()) : "";
			
			// 분할 파일의 마스크(중복 방지) 이름을 설정한다.
			String maskedBlobName = extension.equals("") ? String.valueOf(blobIndex) : blobIndex + "." + extension;
			
			// TODO 아래와 같이 상대 경로를 날짜로 하면 날짜가 넘어가는 시점에 분할 업로드할 경우 정상적으로 업로드가 되지 않는다!!
			
			// 업로드를 위한 상대 경로를 구성한다.
			String relativePath = this.formatRelativePath(request.getSession());
			
			// 분할 업로드를 위한 절대 경로를 구성한다.
			String blobUploadPath = MultipleFileSupportMultipartResolver.getUploadRootDirectory() + relativePath + fileId + File.separator;
			
			File blobUploadDirectory = new File(blobUploadPath);
			
			if (!blobUploadDirectory.exists())
			{
				blobUploadDirectory.mkdirs();
			}
			
			blobOutput = new FileOutputStream(blobUploadPath + maskedBlobName);
			
			FileCopyUtils.copy(file.getInputStream(), blobOutput);
			
			archive.setFileSize(file.getFileItem().getSize());
			
			// 모든 blob이 업로드 완료된 경우, 분할된 파일들을 최종 파일로 만든다.
			if (blobIndex == blobCount)
			{
				FileOutputStream output = null;
				
				File directory = new File(blobUploadPath);
				
				try
				{
					// 최종 업로드된 파일의 마스크 이름을 구성한다.
					String maskedName = extension.equals("") ? GuidUtil.generateGuid() : GuidUtil.generateGuid() + "." + extension;
					
					// 업로드 파일의 스트림을 생성한다.
					output = new FileOutputStream(MultipleFileSupportMultipartResolver.getUploadRootDirectory() + relativePath + maskedName);
					
					// 분할 업로드된 파일들을 순차적으로 가져와 최종 업로드 파일의 스트림에 쓴다.
					for (File item : listNameSortedFiles(directory))
					{
						FileInputStream input = new FileInputStream(item);
						
						byte[] fileArray = FileCopyUtils.copyToByteArray(input);
						
						output.write(fileArray);
					}
					
					File uploadFile = new File(MultipleFileSupportMultipartResolver.getUploadRootDirectory() + relativePath + maskedName);
					
					archive = this.saveArchive(fileName, extension, maskedName, new MimetypesFileTypeMap().getContentType(uploadFile),
							relativePath, uploadFile.length(), request.getRemoteHost());
				}
				finally
				{
					if (output != null)
					{
						output.close();
					}
					
					// 분할 파일들을 모두 삭제한다.
					FileUtils.deleteDirectory(directory);
				}
			}
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
		}
		finally
		{
			if (blobOutput != null)
			{
				blobOutput.close();
			}
		}
		
		return archive;
	}
	
	/**
	 * 단일 파일을 업로드한다.
	 * @param file 업로드하려는 파일 정보 인스턴스를 나타낸다.
	 * @param request 멀티파트 요청으로 멀티파트 분석을 하지 않은 요청 인스턴스이다.
	 * @return 업로드를 완료한 파일의 아카이브 정보 인스턴스이다.
	 * @throws FileNotFoundException 업로드 파일을 생성하지 못했을 때 발생할 수 있다.
	 * @throws IOException 업로드 파일에 바이너리 데이터를 쓸 때 발생할 수 있다.
	 */
	private Archive uploadFile(CommonsMultipartFile file, HttpServletRequest request) throws FileNotFoundException, IOException
	{
		Archive archive = null;
		FileOutputStream output = null;
		
		try
		{
			String originalName = file.getFileItem().getName();
			
			// 업로드하려는 파일의 확장자를 추려낸다.
			String extension = originalName.lastIndexOf('.') > -1 ? originalName.substring(originalName.lastIndexOf('.') + 1, originalName.length()) : "";
			
			// 업로드하려는 파일의 마스크 이름을 구성한다.
			String maskedName = extension.equals("") ? GuidUtil.generateGuid() : GuidUtil.generateGuid() + "." + extension;
			
			// 업로드를 위한 상대 경로를 구성한다.
			String relativePath = this.formatRelativePath(request.getSession());
			
			output = new FileOutputStream(MultipleFileSupportMultipartResolver.getUploadRootDirectory() + relativePath + maskedName);
			
			FileCopyUtils.copy(file.getInputStream(), output);
			
			archive = this.saveArchive(originalName, extension, maskedName,
					file.getContentType(), relativePath, file.getSize(), request.getRemoteHost());
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
		}
		finally
		{
			if (output != null)
			{
				output.close();
			}
		}
		
		return archive;
	}
	
	/**
	 * 요청된 아카이브 아이디에 상응하는 파일을 다운로드한다.
	 * @param archiveId 아카이브의 아이디로 한 개의 파일을 나타내는 대리자이다.
	 * @return 파일 스트리밍을 위한 뷰를 호출한다.
	 */
	@RequestMapping(value="/download", method=RequestMethod.GET)
	public ModelAndView download(@RequestParam("archiveId")int archiveId)
	{
		// TODO 파일 다운로드 실패에 대한 처리 프로세스가 필요!(안내 페이지 등?)
		if (archiveId < 1) return null;
		
		Map<String, Object> result = new HashMap<String, Object>();
		
		try
		{
			// 아카이브 아이디에 상응하는 아카이브 인스턴스를 가져온다.
			Archive archive = this._multipartService.getArchive(archiveId);
			
			// 아카이브와 업로드 처리 정보를 뷰로 전달하기 위해 설정한다.
			result.put(OutputFileStreamView._ARCHIVE_ID_PARAMETER_NAME, archiveId);
			result.put(OutputFileStreamView._ARCHIVE_PARAMETER_NAME, archive);
			result.put(OutputFileStreamView._UPLOAD_ROOT_PARAMETER_NAME, MultipleFileSupportMultipartResolver.getUploadRootDirectory());
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
		}
		
		return new ModelAndView("fileOutputStreamView", result);
	}
	
	/**
	 * 업로드된 파일의 정보를 아카이브에 저장한다.
	 * @param fileName 파일의 실제 이름을 나타낸다.
	 * @param extension 콤마를 뺀 파일의 확장자를 나타낸다. 
	 * @param maskedName 파일 이름 중복을 피하기 위한 변환된 이름을 나타낸다.
	 * @param contentType 파일의 MIME 타입을 나타낸다.
	 * @param relativePath 업로드된 파일이 저장되는 상대 경로를 나타낸다.
	 * @param fileSize 파일의 실제 크기를 나타낸다.
	 * @param uploadedDate 파일이 업로드된 날짜/시간으로 보통 현재 날짜/시간으로 설정한다.
	 * @param uploadIp 파일을 업로드한 사용자의 아이피를 나타낸다.
	 * @return 저장된 아카이브의 정보로 성공할 경우 아카이브 아이디가 1 이상이다.
	 */
	public Archive saveArchive(String fileName, String extension, String maskedName,
			String contentType, String relativePath, long fileSize, String uploadIp)
	{
		Archive archive = new Archive();
		
		archive.setOriginalName(fileName);
		archive.setExtension(extension);
		archive.setMaskedName(maskedName);
		archive.setContentType(contentType);
		archive.setRelativePath(relativePath);
		archive.setFileSize(fileSize);
		archive.setUploadedDateTime(new Date());
		archive.setUploadIp(uploadIp);
		
		this._multipartService.saveArchive(archive);
		
		return archive;
	}
	
	/**
	 * 분할 업로드에 사용되는 메서드이다.
	 * 분할 업로드 디렉토리에 속한 파일의 목록을 파일 이름 순으로 정렬하여 가져온다.
	 * @param directory 분할 업로드 디렉토리로 분할 업로드 시 생성되고 분할 업로드가 끝나면 삭제된다.
	 * @return 정렬된 파일 목록으로 업로드된 순서 별로 정렬된 상태이어야 한다.
	 */
	private static File[] listNameSortedFiles(File directory)
	{
		if (!directory.isDirectory())
		{
			return null;
		}
		
		File[] files = directory.listFiles();
		
		// 파일들을 이름에 의해서 순차 정렬한다.
		Arrays.sort(files, new Comparator<File>()
		{
			/**
			 * 두 파일을 이름을 비교한다.
			 * @param file1 비교하려는 첫 번째 파일을 나타낸다.
			 * @param file2 비교하려는 두 번째 파일을 나타낸다.
			 * @return 0보다 작으면 첫 번째 파일이 순서 상 앞서고 0보다 크면 두 번째 파일이 앞선다. 
			 */
			public int compare(File file1, File file2)
			{
				// 비교하고자 하는 두 파일의 경로와 확장자를 제외한 파일명을 가져온다.
				String file1Name = FilenameUtils.getBaseName(file1.getName());
				String file2Name = FilenameUtils.getBaseName(file2.getName());
				
				Integer file1NameNumber = Integer.parseInt(file1Name);
				Integer file2NameNumber = Integer.parseInt(file2Name);
				
				// 정수형으로 변환된 파일명을 비교한다.
				return file1NameNumber.compareTo(file2NameNumber);
			}
		});
		
		return files;
	}
	
	/**
	 * 업로드할 때 필요한 날짜 기반의 상대 경로를 구성한다.
	 * @param session HTTP 요청에 대한 HTTP 세션을 나타낸다.
	 * @return 구성된 상대로 경로로 날짜를 기반으로 하여 구성된다.
	 * @throws SecurityException 구성된 상대 경로에 대한 절대 경로가 존재하지 않으면 경로를 생성하는데, 이 때 경로에 대한 권한이 없어 발생할 수 있는 예외이다. 
	 */
	private String formatRelativePath(HttpSession session) throws SecurityException
	{
		String path = "";
		
		try
		{
			// 현재 날짜/시간을 가져온다. 
			Calendar calendar = Calendar.getInstance();
			
			// 현재 시간을 기반으로 상대 경로를 구성한다. 
			path = File.separator + calendar.get(Calendar.YEAR)
				+ File.separator + (calendar.get(Calendar.MONTH) + 1)
				+ File.separator + calendar.get(Calendar.DATE) + File.separator;
			
			// 상대 경로에 대한 절대 경로를 구성한다.
			String absolutePath = MultipleFileSupportMultipartResolver.getUploadRootDirectory().equals("")
								? session.getServletContext().getRealPath("/") + path
								: MultipleFileSupportMultipartResolver.getUploadRootDirectory() + File.separator + path;
			
			File absolutePathFile = new File(absolutePath);
			
			// 절대 경로가 존재하지 않는 경우, 해당 경로를 생성한다.
			if (!absolutePathFile.exists())
			{
				absolutePathFile.mkdirs();
			}
		}
		catch (SecurityException error)
		{
			logger.error(error.getMessage(), error);
		}
		
		return path;
	}
}